package lol.clann.minecraft.springboot.plugin.module.devtools.commands;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketListener;
import com.ruoshui.utils.java.reflect.ReflectUtils;
import lol.clann.minecraft.springboot.api.annotation.command.Command;
import lol.clann.minecraft.springboot.api.annotation.command.CommandDefine;
import lol.clann.minecraft.springboot.api.annotation.command.Sender;
import lol.clann.minecraft.springboot.api.bukkit.command.tabcomplete.AbstractListTabComplete;
import lol.clann.minecraft.springboot.api.bukkit.command.tabcomplete.BooleanTabComplete;
import lol.clann.minecraft.springboot.api.bukkit.utils.PluginUtils;
import lol.clann.minecraft.springboot.api.model.core.SBPConfig;
import relocate.com.fasterxml.jackson.databind.ObjectMapper;
import relocate.com.fasterxml.jackson.databind.ObjectWriter;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;

/**
 * 有ProtocolLib插件时,才会加载
 * @author pyz
 * @date 2019/5/19 4:52 PM
 */
@Slf4j
@ConditionalOnClass(name = "com.comphenix.protocol.ProtocolLibrary")
@CommandDefine(name = "devtools", order = 0)
public class PacketDebugger {

    private PacketContext clientContext = new PacketContext();
    private PacketContext serverContext = new PacketContext();

    private ProtocolManager protocolManager;
    @Autowired
    private PluginUtils pluginUtils;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private SBPConfig config;

    private ObjectWriter objectWriter;

    @PostConstruct
    private void init() throws IllegalAccessException {
        //取所有的数据包类
        List<Field> clientPackets = ReflectUtils.getAllFields(PacketType.Play.Client.class, wf -> wf.getType() == PacketType.class && Modifier.isStatic(wf.getModifiers()) && Modifier.isFinal(wf.getModifiers()));
        for (Field packet : clientPackets) {
            PacketType pt = (PacketType) packet.get(null);
            register(clientContext, pt);
        }
        List<Field> serverPackets = ReflectUtils.getAllFields(PacketType.Play.Server.class, wf -> wf.getType() == PacketType.class && Modifier.isStatic(wf.getModifiers()) && Modifier.isFinal(wf.getModifiers()));
        for (Field packet : serverPackets) {
            PacketType pt = (PacketType) packet.get(null);
            register(serverContext, pt);
        }
        protocolManager = ProtocolLibrary.getProtocolManager();
        objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
    }

    private void register(PacketContext context, PacketType pt) {
        context.packetTypes.add(pt);
        context.tabCompleteCandidates.add(pt.name());
        context.nameToStatusMap.put(pt.name(), false);
        context.nameToPacketTypeMap.put(pt.name(), pt);
        List<Field> fields = ReflectUtils.getAllFields(pt.getPacketClass(), f -> !Modifier.isStatic(f.getModifiers()) && !Modifier.isFinal(f.getModifiers()));
        for (Field field : fields) {
            field.setAccessible(true);
        }
        context.nameToFieldsMap.put(pt.name(), fields);
        if (config.isDebugEventDetect()) {
            LOGGER.info("detect {} fields in {} packet:{}", fields.size(), pt.isClient() ? "client" : "server", pt.name());
        }
    }

    @Command(showArgs = "packet <server|client> <packetName|all> <true/false>", des = "开启/关闭指定数据包的调试信息", tabComplete = PacketTabComplete.class)
    private boolean packet(@Sender CommandSender sender, String type, String packetName, boolean status) {
        PacketContext context;
        if (type.equalsIgnoreCase("client")) {
            context = clientContext;
        } else if (type.equalsIgnoreCase("server")) {
            context = serverContext;
        } else {
            sender.sendMessage("未知数据包类型,可选值:server|client");
            return false;
        }
        if (!packetName.equalsIgnoreCase("all")) {
            PacketType packetType = context.nameToPacketTypeMap.get(packetName);
            if (packetType == null) {
                sender.sendMessage("未知数据包:" + type + "-" + packetName);
                return false;
            }
            changeStatus(sender, type, context, packetType, status);
        } else {
            for (PacketType packetType : context.nameToPacketTypeMap.values()) {
                changeStatus(sender, type, context, packetType, status);
            }
        }
        return true;
    }

    private void changeStatus(CommandSender sender, String type, PacketContext context, PacketType packetType, boolean status) {
        String packetName = packetType.name();
        boolean currentStatus = context.nameToStatusMap.getOrDefault(packetName, false);
        if (status) {
            if (currentStatus || context.listenerMap.containsKey(packetName)) {
                sender.sendMessage("该数据包已监听,请勿重复操作");
                return;
            } else {
                context.nameToStatusMap.put(packetName, true);
                registerLisetner(packetType);
                sender.sendMessage("开始监听:" + type + "-" + packetName);
                return;
            }
        } else {
            context.nameToStatusMap.put(packetName, false);
            PacketListener listener = context.listenerMap.remove(packetName);
            if (listener != null) {
                protocolManager.removePacketListener(listener);
            }
            sender.sendMessage("取消监听:" + type + "-" + packetName);
            return;
        }
    }

    private void registerLisetner(PacketType packetType) {
        PacketContext context = packetType.isClient() ? clientContext : serverContext;
        if (context.listenerMap.containsKey(packetType.name())) {
            return;
        }
        PacketAdapter listener = new PacketAdapter(PacketAdapter.params(pluginUtils.getSelf(), packetType)
                .listenerPriority(ListenerPriority.LOWEST)) {
            @Override
            public void onPacketReceiving(PacketEvent event) {
                print("receive", event.getPacketType().isServer() ? "server" : "client", event);
            }

            @Override
            public void onPacketSending(PacketEvent event) {
                print("send", event.getPacketType().isServer() ? "server" : "client", event);
            }
        };
        protocolManager.addPacketListener(listener);
        context.listenerMap.put(packetType.name(), listener);
    }

    @SneakyThrows
    private void print(String action, String sender, PacketEvent event) {
        PacketContext context = event.getPacketType().isClient() ? clientContext : serverContext;
        Player player = event.getPlayer();
        Object packet = event.getPacket().getHandle();
        Map<String, String> info = new LinkedHashMap<>();
        List<Field> fields = context.nameToFieldsMap.get(event.getPacketType().name());
        if (fields != null) {
            for (Field field : fields) {
                info.put(field.getName(), String.valueOf(field.get(packet)));
            }
        }
        info.put("sender", sender);
        info.put("action", action);
        info.put("packetClass", packet.getClass().getName());
        info.put("packetName", event.getPacketType().name());
        String json = objectWriter.writeValueAsString(info);
        player.sendMessage(json);
        player.sendMessage(String.valueOf(System.currentTimeMillis()));
        Bukkit.getConsoleSender().sendMessage(json);
        Bukkit.getConsoleSender().sendMessage(String.valueOf(System.currentTimeMillis()));
    }

    @Component
    @ConditionalOnClass(name = "com.comphenix.protocol.ProtocolLibrary")
    public static class PacketTabComplete extends AbstractListTabComplete {
        private List<String> senderCandidates = new ArrayList<>();

        {
            senderCandidates.add("server");
            senderCandidates.add("client");
        }

        @Autowired
        private PacketDebugger packetDebugger;

        @Override
        protected List<String> getCandidates(CommandSender sender, String alias, String[] args) {
            if (args.length > 4) {
                return new ArrayList<>();
            }
            if (args.length == 2) {
                return senderCandidates;
            } else if (args.length == 3) {
                if (args[1].equalsIgnoreCase("server")) {
                    return packetDebugger.serverContext.tabCompleteCandidates;
                } else {
                    return packetDebugger.clientContext.tabCompleteCandidates;
                }
            } else {
                return BooleanTabComplete.booleanCandidates;
            }
        }
    }

    @Getter
    @ToString
    private static class PacketContext {
        private List<PacketType> packetTypes = new ArrayList<>();
        private List<String> tabCompleteCandidates = new ArrayList<>();
        private Map<String, Boolean> nameToStatusMap = new HashMap<>();
        private Map<String, PacketType> nameToPacketTypeMap = new HashMap<>();
        private Map<String, List<Field>> nameToFieldsMap = new HashMap<>();
        private Map<String, PacketListener> listenerMap = new HashMap<>();
    }
}
